// Module containing instructions for Robot Karel

// import Karel's module
import Robot = require("./RobotKarel");

// import localization
import Loc = require("../../UI/Localization");

// enumeration type of Karel's exceptions
export enum eKarelException { Tired, NoMark, TooManyMarks, CannotMove };

// Karel's exception class
export class KarelException {
    // type of exception
    private mExceptionType: eKarelException;

    // constructor
    constructor(type: eKarelException) {
        this.mExceptionType = type;
    }

    // property getter for exception type
    public get ExceptionType(): eKarelException {
        return this.mExceptionType;
    }
}

// interface for the execution engine
export interface IExecutionEngine {
    SetRegister(index: number, value: number): void;
    GetRegister(index: number): number;
    CallCommand(name: string): void;
    MoveInstructionIndex(moveOffset: number): void;
    RaiseException(exceptionType: eKarelException);
}

// enum of condition operator
export enum eConditionOperator { Is, IsNot };

// enum of checked condition
export enum eConditionType { Wall, Mark, North, South, West, East };

// class containing condition for White and If instructions
export class Condition {

    // condition private attributes
    private mOperator: eConditionOperator;
    private mConditionType: eConditionType;

    // localization module
    private mLocalization: Loc.Localization;

    // constructor
    constructor(operator: eConditionOperator, condType: eConditionType) {
        this.mOperator = operator;
        this.mConditionType = condType;
        this.mLocalization = Loc.Localization.Instance;
    }

    // evaluates the condition 
    public Evaluate(karel: Robot.RobotKarel) {
        var eval: boolean = false;

        // choose the right conditiont type and evaluate
        switch (this.mConditionType) {
            case eConditionType.Wall:
                eval = karel.CanMove() == false;
                break;
            case eConditionType.Mark:
                eval = karel.CanPickMark() == true;
                break;
            case eConditionType.North:
                eval = karel.Direction == Robot.eKarelDirection.North;
                break;
            case eConditionType.South:
                eval = karel.Direction == Robot.eKarelDirection.South;
                break;
            case eConditionType.East:
                eval = karel.Direction == Robot.eKarelDirection.East;
                break;
            case eConditionType.West:
                eval = karel.Direction == Robot.eKarelDirection.West;
                break;
            default: 
                throw "Unknown condition type."
        } // switch

        // combine evalution with the operator
        return this.mOperator == eConditionOperator.Is ? eval : !eval;
    }

    // converts conditon to string
    public ToString(): string {
        var source = this.mOperator == eConditionOperator.Is ? this.GetCmd("cmdIs") : this.GetCmd("cmdIsNot");
        source += " ";

        switch (this.mConditionType) {
            case eConditionType.Wall:
                source += this.GetCmd("cmdWall");
                break;
            case eConditionType.Mark:
                source += this.GetCmd("cmdMark");
                break;
            case eConditionType.North:
                source += this.GetCmd("cmdNorth");
                break;
            case eConditionType.South:
                source += this.GetCmd("cmdSouth");
                break;
            case eConditionType.East:
                source += this.GetCmd("cmdEast");
                break;
            case eConditionType.West:
                source += this.GetCmd("cmdWest");
                break;
        }

        return source.toLocaleUpperCase();
    }

    // returns the localized command
    private GetCmd(key: string): string {
        return this.mLocalization.Get(key);
    }
}

// Base abstract instruction class
export class Instruction {

    // abstract execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {
        throw "Abstract method.";
    }
}

// internal command type
export enum eInternalCommand { Move, TurnLeft, Put, Pick };

// Karel's elementary command instruction
export class InternalCommandInstruction extends Instruction {

    // attributes
    private mCommandType: eInternalCommand;

    // constructor
    public constructor(commandType: eInternalCommand) {
        super();
        this.mCommandType = commandType;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {
        var result = false;

        // choose the command by type
        switch (this.mCommandType) {
            case eInternalCommand.Move:
                if (karel.CanMove()) {
                    karel.Move();
                    result = true;
                } else {
                    engine.RaiseException(eKarelException.CannotMove);
                }
                break;
            case eInternalCommand.TurnLeft:
                karel.TurnLeft();
                result = true;
                break;
            case eInternalCommand.Put:
                if (karel.CanPutMark()) {
                    karel.PutMark();
                } else {
                    engine.RaiseException(eKarelException.TooManyMarks);
                }
                break;
            case eInternalCommand.Pick:
                if (karel.CanPickMark()) {
                    karel.PickMark();
                } else {
                    engine.RaiseException(eKarelException.NoMark);
                }
                break;
            default:
                throw "Unknown Command.";
        }

        // move the instruction pointer
        engine.MoveInstructionIndex(1);

        // invoke redrawing the city and waiting for the next execution tick
        return result;
    }
}

// sub-program call instruction
export class CallInstruction extends Instruction {

    // subcommand name
    private mCommandName: string;

    // constructor
    public constructor(commandName: string) {
        super();
        this.mCommandName = commandName;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {
        // move instruction pointer
        engine.MoveInstructionIndex(1);

        // call the command
        engine.CallCommand(this.mCommandName);

        // no next execution tick
        return false;
    }
}

// end of block instruction
export class EndOfBlockInstruction extends Instruction {

    // instruction pointer offset
    private mMoveOffset: number;

    // constructor
    public constructor(moveOffset: number) {
        super();
        this.mMoveOffset = moveOffset;
    }

    // sets the move offset
    public SetOffset(offset: number) {
        this.mMoveOffset = offset;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {

        // move the instruction pointer
        engine.MoveInstructionIndex(this.mMoveOffset);

        // no next tick
        return false;
    }
}

// repeat instruction class
export class RepeatInstruction extends Instruction {

    // attribute
    private mNumRepetitions: number;
    private mIterationCountRegister: number;
    private mEndOfBlockIndex: number;

    // constructor
    public constructor(repetitions: number, iterationRegister: number) {
        super();
        this.mNumRepetitions = repetitions;
        this.mIterationCountRegister = iterationRegister;
    }

    // sets the end of block index
    public SetEndOfBlockIndex(index: number) {
        this.mEndOfBlockIndex = index;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {
        // iteration index
        var iteration = engine.GetRegister(this.mIterationCountRegister);
        // increment
        iteration++;
        engine.SetRegister(this.mIterationCountRegister, iteration);

        // if we should terminate the block
        if (iteration > this.mNumRepetitions) {
            engine.MoveInstructionIndex(this.mEndOfBlockIndex);
        } else {
            engine.MoveInstructionIndex(1);
        }

        return false;
    }
}

// decision If instruction
export class IfInstruction extends Instruction {    
    // attributes
    private mCondition: Condition;
    private mElseBlockOffset: number;
    private mEndOfBlockOffset: number;

    // constructor
    public constructor(condition: Condition) {
        super();
        this.mCondition = condition;
        this.mElseBlockOffset = null;
        this.mEndOfBlockOffset = null;
    }

    // sets the else block offset
    public SetElseBlockOffset(offset: number) {
        this.mElseBlockOffset = offset;
    }

    // sets the end block offset
    public SetEndBlockOffset(offset: number) {
        this.mEndOfBlockOffset = offset;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {
        // check the condition
        if (this.mCondition.Evaluate(karel) == true) {
            engine.MoveInstructionIndex(1);
        }
        else {
            // check else block existence
            if (this.mElseBlockOffset != null) {
                engine.MoveInstructionIndex(this.mElseBlockOffset);
            }
            else {
                engine.MoveInstructionIndex(this.mEndOfBlockOffset);
            }          
        }

        // no next execution tick
        return false;
    }
}

// While Instruction
export class WhileInstruction extends Instruction {

    // private attributes
    private mCondition: Condition;
    private mEndOfBlockOffset: number;

    // constructor
    public constructor(condition: Condition) {
        super();
        this.mCondition = condition;
    }

    // sets the end of block index
    public SetEndOfBlockOffset(offset: number) {
        this.mEndOfBlockOffset = offset;
    }

    // execution method
    public Execute(karel: Robot.RobotKarel, engine: IExecutionEngine): boolean {

        // evaluate condition
        if (this.mCondition.Evaluate(karel) == true) {
            // continue in the cycle
            engine.MoveInstructionIndex(1);
        }
        else {
            engine.MoveInstructionIndex(this.mEndOfBlockOffset);
        }

        return false;
    }
}

// the set of instructions - one compiled command
export class InstructionSet {
    
    // private attributes
    private mName: string;
    private mInstructions: Array<Instruction>;
    private mRegisterCount: number;

    // constructor
    public constructor(name: string) {
        this.mName = name;
        this.mInstructions = new Array<Instruction>();
        this.mRegisterCount = 0;
    }

    // property getter returning the command name
    public get Name() {
        return this.mName;
    }

    // property getter for the register count
    public get RegisterCount() {
        return this.mRegisterCount;
    }

    // returns instruction count
    public get InstructionCount() {
        return this.mInstructions.length;
    }

    // returns the instruction at given index
    public GetInstruction(index: number): Instruction {
        return this.mInstructions[index];
    }

    // adds one instruction
    public AddInstruction(instruction: Instruction): void {
        this.mInstructions.push(instruction);
    }

    // sets the value of register count
    public SetRegisterCount(count: number): void {
        this.mRegisterCount = count;
    }
}
